/*
 * Copyright (c) 2008, 2020 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.grizzly.nio.tmpselectors;

import java.io.IOException;
import java.net.SocketAddress;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.concurrent.TimeUnit;

import org.glassfish.grizzly.AbstractWriter;
import org.glassfish.grizzly.Buffer;
import org.glassfish.grizzly.CompletionHandler;
import org.glassfish.grizzly.Connection;
import org.glassfish.grizzly.WriteHandler;
import org.glassfish.grizzly.WriteResult;
import org.glassfish.grizzly.asyncqueue.MessageCloner;
import org.glassfish.grizzly.asyncqueue.WritableMessage;
import org.glassfish.grizzly.nio.NIOConnection;

/**
 *
 * @author oleksiys
 */
@SuppressWarnings("deprecation")
public abstract class TemporarySelectorWriter extends AbstractWriter<SocketAddress> {

    protected final TemporarySelectorsEnabledTransport transport;

    public TemporarySelectorWriter(TemporarySelectorsEnabledTransport transport) {
        this.transport = transport;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void write(final Connection<SocketAddress> connection, final SocketAddress dstAddress, final WritableMessage message,
            final CompletionHandler<WriteResult<WritableMessage, SocketAddress>> completionHandler, final MessageCloner<WritableMessage> messageCloner) {

        write(connection, dstAddress, message, completionHandler, null, connection.getWriteTimeout(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS);
    }

    /**
     * {@inheritDoc}
     */
    @Deprecated
    @Override
    public void write(Connection<SocketAddress> connection, SocketAddress dstAddress, WritableMessage message,
            CompletionHandler<WriteResult<WritableMessage, SocketAddress>> completionHandler,
            final org.glassfish.grizzly.asyncqueue.PushBackHandler pushBackHandler) {
        write(connection, dstAddress, message, completionHandler, pushBackHandler, connection.getWriteTimeout(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS);
    }

    /**
     * Method writes the {@link WritableMessage} to the specific address.
     *
     * @param connection the {@link org.glassfish.grizzly.Connection} to write to
     * @param dstAddress the destination address the <tt>message</tt> will be sent to
     * @param message the {@link WritableMessage}, from which the data will be written
     * @param completionHandler {@link org.glassfish.grizzly.CompletionHandler}, which will get notified, when write will be
     * completed
     */
    public void write(Connection<SocketAddress> connection, SocketAddress dstAddress, WritableMessage message,
            CompletionHandler<WriteResult<WritableMessage, SocketAddress>> completionHandler, long timeout, TimeUnit timeunit) {
        write(connection, dstAddress, message, completionHandler, null, timeout, timeunit);
    }

    /**
     * Method writes the {@link WritableMessage} to the specific address.
     *
     * @param connection the {@link org.glassfish.grizzly.Connection} to write to
     * @param dstAddress the destination address the <tt>message</tt> will be sent to
     * @param message the {@link WritableMessage}, from which the data will be written
     * @param completionHandler {@link org.glassfish.grizzly.CompletionHandler}, which will get notified, when write will be
     * completed
     */
    public void write(Connection<SocketAddress> connection, SocketAddress dstAddress, WritableMessage message,
            CompletionHandler<WriteResult<WritableMessage, SocketAddress>> completionHandler,
            final org.glassfish.grizzly.asyncqueue.PushBackHandler pushBackHandler, long timeout, TimeUnit timeunit) {

        if (message == null) {
            failure(new IllegalStateException("Message cannot be null"), completionHandler);
            return;
        }

        if (connection == null || !(connection instanceof NIOConnection)) {
            failure(new IllegalStateException("Connection should be NIOConnection and cannot be null"), completionHandler);
            return;
        }

        final NIOConnection nioConnection = (NIOConnection) connection;

        final WriteResult<WritableMessage, SocketAddress> writeResult = WriteResult.create(connection, message, dstAddress, 0);

        try {
            write0(nioConnection, dstAddress, message, writeResult, timeout, timeunit);

            // Call PushBackHandler after data is written.
            // IMO It has more sense for blocking case
            if (pushBackHandler != null) {
                pushBackHandler.onAccept(connection, message);
            }

            if (completionHandler != null) {
                completionHandler.completed(writeResult);
            }

            message.release();
        } catch (IOException e) {
            failure(e, completionHandler);
        }
    }

    /**
     * Flush the buffer by looping until the {@link Buffer} is empty
     *
     *
     * @param connection the {@link org.glassfish.grizzly.Connection}.
     * @param dstAddress the destination address.
     * @param message
     * @param currentResult the result of the write operation
     * @param timeout operation timeout value value
     * @param timeunit the timeout unit
     * @return The number of bytes written.
     *
     * @throws java.io.IOException
     */
    protected long write0(final NIOConnection connection, final SocketAddress dstAddress, final WritableMessage message,
            final WriteResult<WritableMessage, SocketAddress> currentResult, final long timeout, final TimeUnit timeunit) throws IOException {

        final SelectableChannel channel = connection.getChannel();
        final long writeTimeout = TimeUnit.MILLISECONDS.convert(timeout, timeunit);

        SelectionKey key = null;
        Selector writeSelector = null;
        int attempts = 0;
        int bytesWritten = 0;

        try {
            // noinspection SynchronizationOnLocalVariableOrMethodParameter
            synchronized (connection) {
                while (message.hasRemaining()) {
                    long len = writeNow0(connection, dstAddress, message, currentResult);

                    if (len > 0) {
                        attempts = 0;
                        bytesWritten += len;
                    } else {
                        attempts++;
                        if (writeSelector == null) {
                            writeSelector = transport.getTemporarySelectorIO().getSelectorPool().poll();

                            if (writeSelector == null) {
                                // Continue using the main one.
                                continue;
                            }
                            key = channel.register(writeSelector, SelectionKey.OP_WRITE);
                        } else {
                            writeSelector.selectedKeys().clear();
                        }

                        if (writeSelector.select(writeTimeout) == 0) {
                            if (attempts > 2) {
                                throw new IOException("Client disconnected");
                            }
                        }
                    }
                }
            }
        } finally {
            transport.getTemporarySelectorIO().recycleTemporaryArtifacts(writeSelector, key);
        }

        return bytesWritten;
    }

    public TemporarySelectorsEnabledTransport getTransport() {
        return transport;
    }

    protected abstract long writeNow0(NIOConnection connection, SocketAddress dstAddress, WritableMessage message,
            WriteResult<WritableMessage, SocketAddress> currentResult) throws IOException;

    private static void failure(final Throwable failure, final CompletionHandler<WriteResult<WritableMessage, SocketAddress>> completionHandler) {
        if (completionHandler != null) {
            completionHandler.failed(failure);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean canWrite(final Connection connection) {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void notifyWritePossible(final Connection connection, final WriteHandler writeHandler) {
        try {
            writeHandler.onWritePossible();
        } catch (Throwable t) {
            writeHandler.onError(t);
        }
    }
}
